Complete Authentication & Authorization Guide
Spring Boot + React with JWT, RBAC, SSO, MFA, and Moreβ
Table of Contentsβ
- Overview
- JWT (JSON Web Tokens)
- HttpOnly Cookies
- Complete Authentication Flow
- Role-Based Access Control (RBAC)
- Single Sign-On (SSO)
- Multi-Factor Authentication (MFA)
- One-Time Password (OTP)
- Complete Code Examples
- Security Best Practices
- Advanced Topics
- Production Checklist
Overviewβ
Authentication vs Authorizationβ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β AUTHENTICATION: "Who are you?" β
β ββ Login with username/password β
β ββ Verify identity β
β ββ Issue token β
β β
β AUTHORIZATION: "What can you do?" β
β ββ Check user roles β
β ββ Verify permissions β
β ββ Allow/deny access β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
| Aspect | Authentication | Authorization |
|---|---|---|
| Purpose | Verify identity | Control access |
| Question | "Who are you?" | "What can you do?" |
| Process | Login, credentials | Check permissions |
| Result | User identified | Access granted/denied |
| Example | Username/password | Admin can delete users |
| Happens | Once (at login) | Every request |
JWT (JSON Web Tokens)β
JWT Structureβ
1. Headerβ
{
"alg": "HS256",
"typ": "JWT"
}
2. Payload (Claims)β
{
"sub": "user123",
"name": "John Doe",
"email": "john@example.com",
"role": "admin",
"iat": 1640000000,
"exp": 1640003600
}
3. Signatureβ
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
Access Token vs Refresh Tokenβ
| Aspect | Access Token | Refresh Token |
|---|---|---|
| Purpose | Access protected resources | Get new access token |
| Lifespan | Short (15 min - 1 hour) | Long (7-30 days) |
| Storage | Memory/state | HttpOnly cookie |
| Sent with | Every API request | Only to /refresh endpoint |
| Payload | User info, roles, permissions | User ID, token ID |
| Revocable | No (stateless) | Yes (stored in DB) |
Token Storage Optionsβ
| Storage | Security | XSS Vulnerable | CSRF Vulnerable | Best For |
|---|---|---|---|---|
| LocalStorage | β Low | β Yes | β No | Never (avoid) |
| SessionStorage | β Low | β Yes | β No | Never (avoid) |
| Memory (state) | β Good | β οΈ Lost on refresh | β No | Access tokens |
| HttpOnly Cookie | β Best | β No | β Yes (use CSRF token) | Refresh tokens |
Recommended Approach:
Access Token β In-memory (React state)
Refresh Token β HttpOnly cookie
HttpOnly Cookiesβ
Security Benefitsβ
βββββββββββββββββββββββββββββββββββββββββββββββ
β Cookie Security Flags β
βββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β HttpOnly: Prevents JavaScript access β
β Secure: Only sent over HTTPS β
β SameSite: Prevents CSRF attacks β
β Domain: Limits cookie scope β
β Path: Restricts cookie path β
β Max-Age: Sets expiration β
β β
βββββββββββββββββββββββββββββββββββββββββββββββ
Complete Authentication Flowβ
Login Flowβ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Login Flow Diagram β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
User React App Spring Boot Database
β β β β
β 1. Enter credentials β β β
βββββββββββββββββββββββββ>β β β
β β 2. POST /api/auth/login β
β ββββββββββββββββββββββ>β β
β β {email, password} β β
β β β 3. Find user β
β β ββββββββββββββββββββ>β
β β β<ββββββββββββββββββββ€
β β β 4. User data β
β β β β
β β β 5. Verify passwordβ
β β β (BCrypt) β
β β β β
β β β 6. Generate tokensβ
β β β - Access token β
β β β - Refresh token β
β β β β
β β β 7. Store refresh β
β β ββββββββββββββββββββ>β
β β β β
β β 8. Response β β
β β<ββββββββββββββββββββββ€ β
β β { β β
β β accessToken, β β
β β user: {...} β β
β β } β β
β β + Set-Cookie: β β
β β refreshToken β β
β β β β
β 9. Login success β 10. Store access β β
β<βββββββββββββββββββββββββ€ token in state β β
β β 11. Redirect to β β
β β dashboard β β
Role-Based Access Control (RBAC)β
RBAC Hierarchyβ
βββββββββββββββββ ββββββββββββββββββββββββββββββββββββββββββββββ
β RBAC Hierarchy β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
User
β
has β
β
Role βββββββββββββββββββ
β β
has β has β
β β
Permissions Inherited
from other roles
Example:
Admin Role
βββ user:create
βββ user:read
βββ user:update
βββ user:delete
βββ post:create
βββ post:read
βββ post:update
βββ post:delete
Editor Role
βββ post:create
βββ post:read
βββ post:update
βββ user:read (own)
Viewer Role
βββ post:read
βββ user:read (own)
Complete Code Examplesβ
Spring Boot Backend Implementationβ
1. Project Dependencies (pom.xml)β
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
<groupId>com.example</groupId>
<artifactId>auth-service</artifactId>
<version>1.0.0</version>
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Starter Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Spring Boot Starter Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Spring Boot Starter Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<!-- PostgreSQL Driver -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Google Authenticator (for TOTP/MFA) -->
<dependency>
<groupId>com.warrenstrange</groupId>
<artifactId>googleauth</artifactId>
<version>1.5.0</version>
</dependency>
<!-- OAuth2 Client (for SSO) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
</dependencies>
</project>
2. Application Propertiesβ
# application.properties
# Server Configuration
server.port=8080
server.servlet.context-path=/api
# Database Configuration
spring.datasource.url=jdbc:postgresql://localhost:5432/authdb
spring.datasource.username=postgres
spring.datasource.password=password
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
# JWT Configuration
jwt.secret=your-256-bit-secret-key-change-this-in-production
jwt.access-token-expiration=900000
jwt.refresh-token-expiration=604800000
# Cookie Configuration
cookie.secure=true
cookie.http-only=true
cookie.same-site=Strict
cookie.max-age=604800
# OAuth2 Configuration (Google)
spring.security.oauth2.client.registration.google.client-id=your-google-client-id
spring.security.oauth2.client.registration.google.client-secret=your-google-client-secret
spring.security.oauth2.client.registration.google.scope=profile,email
# OAuth2 Configuration (GitHub)
spring.security.oauth2.client.registration.github.client-id=your-github-client-id
spring.security.oauth2.client.registration.github.client-secret=your-github-client-secret
spring.security.oauth2.client.registration.github.scope=read:user,user:email
# CORS Configuration
cors.allowed-origins=http://localhost:3000,http://localhost:5173
cors.allowed-methods=GET,POST,PUT,DELETE,OPTIONS
cors.allowed-headers=*
cors.allow-credentials=true
3. Entity Modelsβ
// User.java
package com.example.auth.entity;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.Set;
@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Column(unique = true, nullable = false)
private String email;
@Column(nullable = false)
private String password;
private String name;
@Column(name = "phone_number")
private String phoneNumber;
@Column(name = "is_email_verified")
private boolean isEmailVerified = false;
@Column(name = "is_mfa_enabled")
private boolean isMfaEnabled = false;
@Column(name = "mfa_secret")
private String mfaSecret;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<Role> roles = new HashSet<>();
@Column(name = "created_at")
private LocalDateTime createdAt = LocalDateTime.now();
@Column(name = "updated_at")
private LocalDateTime updatedAt = LocalDateTime.now();
@PreUpdate
public void preUpdate() {
this.updatedAt = LocalDateTime.now();
}
}
// Role.java
package com.example.auth.entity;
import jakarta.persistence.*;
import lombok.*;
import java.util.HashSet;
import java.util.Set;
@Entity
@Table(name = "roles")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Column(unique = true, nullable = false)
@Enumerated(EnumType.STRING)
private RoleType name;
private String description;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "role_permissions",
joinColumns = @JoinColumn(name = "role_id"),
inverseJoinColumns = @JoinColumn(name = "permission_id")
)
private Set<Permission> permissions = new HashSet<>();
}
// RoleType.java
package com.example.auth.entity;
public enum RoleType {
ADMIN,
EDITOR,
VIEWER,
USER
}
// Permission.java
package com.example.auth.entity;
import jakarta.persistence.*;
import lombok.*;
@Entity
@Table(name = "permissions")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Permission {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Column(unique = true, nullable = false)
private String name;
private String resource;
private String action;
private String description;
}
// RefreshToken.java
package com.example.auth.entity;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "refresh_tokens")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class RefreshToken {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Column(nullable = false, unique = true)
private String token;
@ManyToOne
@JoinColumn(name = "user_id", nullable = false)
private User user;
@Column(name = "expires_at", nullable = false)
private LocalDateTime expiresAt;
@Column(name = "created_at")
private LocalDateTime createdAt = LocalDateTime.now();
public boolean isExpired() {
return LocalDateTime.now().isAfter(expiresAt);
}
}
// OtpToken.java
package com.example.auth.entity;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "otp_tokens")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class OtpToken {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Column(nullable = false)
private String otp;
@Column(nullable = false)
private String email;
@Enumerated(EnumType.STRING)
private OtpType type;
@Column(name = "expires_at")
private LocalDateTime expiresAt;
@Column(name = "created_at")
private LocalDateTime createdAt = LocalDateTime.now();
private boolean used = false;
public boolean isExpired() {
return LocalDateTime.now().isAfter(expiresAt);
}
}
// OtpType.java
package com.example.auth.entity;
public enum OtpType {
EMAIL_VERIFICATION,
PASSWORD_RESET,
MFA_LOGIN,
PHONE_VERIFICATION
}
4. Repository Interfacesβ
// UserRepository.java
package com.example.auth.repository;
import com.example.auth.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, String> {
Optional<User> findByEmail(String email);
boolean existsByEmail(String email);
}
// RoleRepository.java
package com.example.auth.repository;
import com.example.auth.entity.Role;
import com.example.auth.entity.RoleType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface RoleRepository extends JpaRepository<Role, String> {
Optional<Role> findByName(RoleType name);
}
// PermissionRepository.java
package com.example.auth.repository;
import com.example.auth.entity.Permission;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface PermissionRepository extends JpaRepository<Permission, String> {
Optional<Permission> findByName(String name);
}
// RefreshTokenRepository.java
package com.example.auth.repository;
import com.example.auth.entity.RefreshToken;
import com.example.auth.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, String> {
Optional<RefreshToken> findByToken(String token);
void deleteByUser(User user);
void deleteByToken(String token);
}
// OtpTokenRepository.java
package com.example.auth.repository;
import com.example.auth.entity.OtpToken;
import com.example.auth.entity.OtpType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface OtpTokenRepository extends JpaRepository<OtpToken, String> {
Optional<OtpToken> findByEmailAndOtpAndType(String email, String otp, OtpType type);
void deleteByEmail(String email);
}
5. JWT Utilityβ
// JwtUtil.java
package com.example.auth.util;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.access-token-expiration}")
private Long accessTokenExpiration;
@Value("${jwt.refresh-token-expiration}")
private Long refreshTokenExpiration;
private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(secret.getBytes());
}
// Generate Access Token
public String generateAccessToken(UserDetails userDetails, Map<String, Object> claims) {
return createToken(claims, userDetails.getUsername(), accessTokenExpiration);
}
// Generate Refresh Token
public String generateRefreshToken(String username) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, username, refreshTokenExpiration);
}
// Create Token
private String createToken(Map<String, Object> claims, String subject, Long expiration) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration);
return Jwts.builder()
.claims(claims)
.subject(subject)
.issuedAt(now)
.expiration(expiryDate)
.signWith(getSigningKey())
.compact();
}
// Extract Username
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
// Extract Expiration
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
// Extract Claim
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
// Extract All Claims
private Claims extractAllClaims(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
// Check if Token is Expired
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
// Validate Token
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}
6. Security Configurationβ
// SecurityConfig.java
package com.example.auth.config;
import com.example.auth.security.JwtAuthenticationEntryPoint;
import com.example.auth.security.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthFilter;
private final UserDetailsService userDetailsService;
private final JwtAuthenticationEntryPoint authenticationEntryPoint;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/auth/login",
"/auth/register",
"/auth/refresh",
"/auth/forgot-password",
"/auth/reset-password",
"/auth/verify-email",
"/oauth2/**"
).permitAll()
.anyRequest().authenticated()
)
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authenticationProvider(authenticationProvider())
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling(ex ->
ex.authenticationEntryPoint(authenticationEntryPoint)
);
return http.build();
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList(
"http://localhost:3000",
"http://localhost:5173"
));
configuration.setAllowedMethods(Arrays.asList(
"GET", "POST", "PUT", "DELETE", "OPTIONS"
));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
7. JWT Authentication Filterβ
// JwtAuthenticationFilter.java
package com.example.auth.security;
import com.example.auth.util.JwtUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
try {
final String jwt = authHeader.substring(7);
final String username = jwtUtil.extractUsername(jwt);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
} catch (Exception e) {
logger.error("Cannot set user authentication: {}", e);
}
filterChain.doFilter(request, response);
}
}
// JwtAuthenticationEntryPoint.java
package com.example.auth.security;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException
) throws IOException, ServletException {
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("{\"error\": \"Unauthorized\", \"message\": \"" +
authException.getMessage() + "\"}");
}
}
// CustomUserDetailsService.java
package com.example.auth.security;
import com.example.auth.entity.User;
import com.example.auth.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + email));
return org.springframework.security.core.userdetails.User.builder()
.username(user.getEmail())
.password(user.getPassword())
.authorities(getAuthorities(user))
.accountExpired(false)
.accountLocked(false)
.credentialsExpired(false)
.disabled(!user.isEmailVerified())
.build();
}
private Collection<? extends GrantedAuthority> getAuthorities(User user) {
return user.getRoles().stream()
.flatMap(role -> {
var roleAuthority = new SimpleGrantedAuthority("ROLE_" + role.getName());
var permissions = role.getPermissions().stream()
.map(permission -> new SimpleGrantedAuthority(permission.getName()))
.collect(Collectors.toList());
permissions.add(roleAuthority);
return permissions.stream();
})
.collect(Collectors.toSet());
}
}
8. DTOs (Data Transfer Objects)β
// LoginRequest.java
package com.example.auth.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class LoginRequest {
@NotBlank(message = "Email is required")
@Email(message = "Invalid email format")
private String email;
@NotBlank(message = "Password is required")
private String password;
private String mfaCode;
}
// RegisterRequest.java
package com.example.auth.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Data
public class RegisterRequest {
@NotBlank(message = "Name is required")
private String name;
@NotBlank(message = "Email is required")
@Email(message = "Invalid email format")
private String email;
@NotBlank(message = "Password is required")
@Size(min = 8, message = "Password must be at least 8 characters")
private String password;
private String phoneNumber;
}
// AuthResponse.java
package com.example.auth.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AuthResponse {
private String accessToken;
private String tokenType = "Bearer";
private UserDto user;
private boolean mfaRequired;
}
// UserDto.java
package com.example.auth.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Set;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserDto {
private String id;
private String email;
private String name;
private String phoneNumber;
private boolean emailVerified;
private boolean mfaEnabled;
private Set<String> roles;
private Set<String> permissions;
}
// RefreshTokenRequest.java
package com.example.auth.dto;
import lombok.Data;
@Data
public class RefreshTokenRequest {
private String refreshToken;
}
// MessageResponse.java
package com.example.auth.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class MessageResponse {
private String message;
}
// ErrorResponse.java
package com.example.auth.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ErrorResponse {
private String error;
private String message;
private int status;
private LocalDateTime timestamp;
}
9. Authentication Serviceβ
// AuthService.java
package com.example.auth.service;
import com.example.auth.dto.*;
import com.example.auth.entity.*;
import com.example.auth.repository.*;
import com.example.auth.util.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class AuthService {
private final UserRepository userRepository;
private final RoleRepository roleRepository;
private final RefreshTokenRepository refreshTokenRepository;
private final OtpTokenRepository otpTokenRepository;
private final PasswordEncoder passwordEncoder;
private final JwtUtil jwtUtil;
private final AuthenticationManager authenticationManager;
private final EmailService emailService;
private final MfaService mfaService;
@Value("${jwt.refresh-token-expiration}")
private Long refreshTokenExpiration;
@Value("${cookie.max-age}")
private int cookieMaxAge;
// Register new user
@Transactional
public AuthResponse register(RegisterRequest request) {
// Check if user already exists
if (userRepository.existsByEmail(request.getEmail())) {
throw new RuntimeException("Email already registered");
}
// Create new user
User user = User.builder()
.email(request.getEmail())
.name(request.getName())
.password(passwordEncoder.encode(request.getPassword()))
.phoneNumber(request.getPhoneNumber())
.isEmailVerified(false)
.isMfaEnabled(false)
.build();
// Assign default role
Role userRole = roleRepository.findByName(RoleType.USER)
.orElseThrow(() -> new RuntimeException("Default role not found"));
user.getRoles().add(userRole);
userRepository.save(user);
// Send verification email
String otp = generateOtp();
saveOtp(user.getEmail(), otp, OtpType.EMAIL_VERIFICATION);
emailService.sendVerificationEmail(user.getEmail(), otp);
return AuthResponse.builder()
.user(mapToUserDto(user))
.build();
}
// Login
@Transactional
public AuthResponse login(LoginRequest request, HttpServletResponse response) {
// Authenticate user
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getEmail(),
request.getPassword()
)
);
User user = userRepository.findByEmail(request.getEmail())
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
// Check MFA
if (user.isMfaEnabled()) {
if (request.getMfaCode() == null) {
return AuthResponse.builder()
.mfaRequired(true)
.build();
}
boolean isValid = mfaService.verifyTotp(user.getMfaSecret(), request.getMfaCode());
if (!isValid) {
throw new RuntimeException("Invalid MFA code");
}
}
// Generate tokens
Map<String, Object> claims = buildClaims(user);
String accessToken = jwtUtil.generateAccessToken(
(org.springframework.security.core.userdetails.UserDetails) authentication.getPrincipal(),
claims
);
String refreshToken = jwtUtil.generateRefreshToken(user.getEmail());
// Save refresh token
saveRefreshToken(user, refreshToken);
// Set refresh token as HttpOnly cookie
setRefreshTokenCookie(response, refreshToken);
return AuthResponse.builder()
.accessToken(accessToken)
.user(mapToUserDto(user))
.mfaRequired(false)
.build();
}
// Refresh access token
@Transactional
public AuthResponse refreshAccessToken(String refreshToken) {
// Verify refresh token
String username = jwtUtil.extractUsername(refreshToken);
User user = userRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
// Check if refresh token exists in database
RefreshToken storedToken = refreshTokenRepository.findByToken(refreshToken)
.orElseThrow(() -> new RuntimeException("Refresh token not found"));
if (storedToken.isExpired()) {
refreshTokenRepository.delete(storedToken);
throw new RuntimeException("Refresh token expired");
}
// Generate new access token
Map<String, Object> claims = buildClaims(user);
String accessToken = jwtUtil.generateAccessToken(
org.springframework.security.core.userdetails.User.builder()
.username(user.getEmail())
.password(user.getPassword())
.authorities(Collections.emptyList())
.build(),
claims
);
return AuthResponse.builder()
.accessToken(accessToken)
.user(mapToUserDto(user))
.build();
}
// Logout
@Transactional
public void logout(String refreshToken, HttpServletResponse response) {
if (refreshToken != null) {
refreshTokenRepository.deleteByToken(refreshToken);
}
// Clear cookie
Cookie cookie = new Cookie("refreshToken", null);
cookie.setHttpOnly(true);
cookie.setSecure(true);
cookie.setPath("/");
cookie.setMaxAge(0);
response.addCookie(cookie);
}
// Verify email
@Transactional
public MessageResponse verifyEmail(String email, String otp) {
OtpToken otpToken = otpTokenRepository
.findByEmailAndOtpAndType(email, otp, OtpType.EMAIL_VERIFICATION)
.orElseThrow(() -> new RuntimeException("Invalid OTP"));
if (otpToken.isExpired() || otpToken.isUsed()) {
throw new RuntimeException("OTP expired or already used");
}
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
user.setEmailVerified(true);
userRepository.save(user);
otpToken.setUsed(true);
otpTokenRepository.save(otpToken);
return new MessageResponse("Email verified successfully");
}
// Forgot password
@Transactional
public MessageResponse forgotPassword(String email) {
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
String otp = generateOtp();
saveOtp(email, otp, OtpType.PASSWORD_RESET);
emailService.sendPasswordResetEmail(email, otp);
return new MessageResponse("Password reset OTP sent to email");
}
// Reset password
@Transactional
public MessageResponse resetPassword(String email, String otp, String newPassword) {
OtpToken otpToken = otpTokenRepository
.findByEmailAndOtpAndType(email, otp, OtpType.PASSWORD_RESET)
.orElseThrow(() -> new RuntimeException("Invalid OTP"));
if (otpToken.isExpired() || otpToken.isUsed()) {
throw new RuntimeException("OTP expired or already used");
}
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
user.setPassword(passwordEncoder.encode(newPassword));
userRepository.save(user);
otpToken.setUsed(true);
otpTokenRepository.save(otpToken);
// Invalidate all refresh tokens
refreshTokenRepository.deleteByUser(user);
return new MessageResponse("Password reset successful");
}
// Helper methods
private Map<String, Object> buildClaims(User user) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", user.getId());
claims.put("name", user.getName());
claims.put("roles", user.getRoles().stream()
.map(role -> role.getName().toString())
.collect(Collectors.toList()));
claims.put("permissions", user.getRoles().stream()
.flatMap(role -> role.getPermissions().stream())
.map(Permission::getName)
.collect(Collectors.toSet()));
return claims;
}
private UserDto mapToUserDto(User user) {
return UserDto.builder()
.id(user.getId())
.email(user.getEmail())
.name(user.getName())
.phoneNumber(user.getPhoneNumber())
.emailVerified(user.isEmailVerified())
.mfaEnabled(user.isMfaEnabled())
.roles(user.getRoles().stream()
.map(role -> role.getName().toString())
.collect(Collectors.toSet()))
.permissions(user.getRoles().stream()
.flatMap(role -> role.getPermissions().stream())
.map(Permission::getName)
.collect(Collectors.toSet()))
.build();
}
private void saveRefreshToken(User user, String token) {
RefreshToken refreshToken = RefreshToken.builder()
.token(token)
.user(user)
.expiresAt(LocalDateTime.now().plusSeconds(refreshTokenExpiration / 1000))
.build();
refreshTokenRepository.save(refreshToken);
}
private void setRefreshTokenCookie(HttpServletResponse response, String token) {
Cookie cookie = new Cookie("refreshToken", token);
cookie.setHttpOnly(true);
cookie.setSecure(true);
cookie.setPath("/");
cookie.setMaxAge(cookieMaxAge);
response.addCookie(cookie);
}
private String generateOtp() {
Random random = new Random();
return String.format("%06d", random.nextInt(999999));
}
private void saveOtp(String email, String otp, OtpType type) {
otpTokenRepository.deleteByEmail(email);
OtpToken otpToken = OtpToken.builder()
.email(email)
.otp(otp)
.type(type)
.expiresAt(LocalDateTime.now().plusMinutes(10))
.build();
otpTokenRepository.save(otpToken);
}
}
10. MFA Serviceβ
// MfaService.java
package com.example.auth.service;
import com.warrenstrange.googleauth.GoogleAuthenticator;
import com.warrenstrange.googleauth.GoogleAuthenticatorKey;
import com.warrenstrange.googleauth.GoogleAuthenticatorQRGenerator;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class MfaService {
private final GoogleAuthenticator googleAuthenticator = new GoogleAuthenticator();
// Generate MFA secret
public String generateSecret() {
GoogleAuthenticatorKey key = googleAuthenticator.createCredentials();
return key.getKey();
}
// Generate QR code URL
public String generateQrCodeUrl(String email, String secret) {
return GoogleAuthenticatorQRGenerator.getOtpAuthURL(
"YourAppName",
email,
new GoogleAuthenticatorKey.Builder(secret).build()
);
}
// Verify TOTP code
public boolean verifyTotp(String secret, String code) {
try {
int codeInt = Integer.parseInt(code);
return googleAuthenticator.authorize(secret, codeInt);
} catch (NumberFormatException e) {
return false;
}
}
}
11. Email Serviceβ
// EmailService.java
package com.example.auth.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
@Slf4j
public class EmailService {
private final JavaMailSender mailSender;
public void sendVerificationEmail(String to, String otp) {
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(to);
message.setSubject("Email Verification");
message.setText("Your verification code is: " + otp +
"\n\nThis code will expire in 10 minutes.");
try {
mailSender.send(message);
log.info("Verification email sent to: {}", to);
} catch (Exception e) {
log.error("Failed to send verification email: {}", e.getMessage());
}
}
public void sendPasswordResetEmail(String to, String otp) {
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(to);
message.setSubject("Password Reset");
message.setText("Your password reset code is: " + otp +
"\n\nThis code will expire in 10 minutes.");
try {
mailSender.send(message);
log.info("Password reset email sent to: {}", to);
} catch (Exception e) {
log.error("Failed to send password reset email: {}", e.getMessage());
}
}
}
12. Authentication Controllerβ
// AuthController.java
package com.example.auth.controller;
import com.example.auth.dto.*;
import com.example.auth.service.AuthService;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
@PostMapping("/register")
public ResponseEntity<AuthResponse> register(@Valid @RequestBody RegisterRequest request) {
return ResponseEntity.ok(authService.register(request));
}
@PostMapping("/login")
public ResponseEntity<AuthResponse> login(
@Valid @RequestBody LoginRequest request,
HttpServletResponse response) {
return ResponseEntity.ok(authService.login(request, response));
}
@PostMapping("/refresh")
public ResponseEntity<AuthResponse> refresh(HttpServletRequest request) {
String refreshToken = getRefreshTokenFromCookie(request);
if (refreshToken == null) {
return ResponseEntity.status(401).build();
}
return ResponseEntity.ok(authService.refreshAccessToken(refreshToken));
}
@PostMapping("/logout")
public ResponseEntity<MessageResponse> logout(
HttpServletRequest request,
HttpServletResponse response) {
String refreshToken = getRefreshTokenFromCookie(request);
authService.logout(refreshToken, response);
return ResponseEntity.ok(new MessageResponse("Logged out successfully"));
}
@PostMapping("/verify-email")
public ResponseEntity<MessageResponse> verifyEmail(
@RequestParam String email,
@RequestParam String otp) {
return ResponseEntity.ok(authService.verifyEmail(email, otp));
}
@PostMapping("/forgot-password")
public ResponseEntity<MessageResponse> forgotPassword(@RequestParam String email) {
return ResponseEntity.ok(authService.forgotPassword(email));
}
@PostMapping("/reset-password")
public ResponseEntity<MessageResponse> resetPassword(
@RequestParam String email,
@RequestParam String otp,
@RequestParam String newPassword) {
return ResponseEntity.ok(authService.resetPassword(email, otp, newPassword));
}
private String getRefreshTokenFromCookie(HttpServletRequest request) {
if (request.getCookies() == null) {
return null;
}
return Arrays.stream(request.getCookies())
.filter(cookie -> "refreshToken".equals(cookie.getName()))
.findFirst()
.map(Cookie::getValue)
.orElse(null);
}
}
React Frontend Implementationβ
1. Project Setupβ
# Create React app with Vite
npm create vite@latest auth-frontend -- --template react
cd auth-frontend
# Install dependencies
npm install axios react-router-dom @tanstack/react-query
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
2. API Configurationβ
// src/api/axios.js
import axios from 'axios';
const api = axios.create({
baseURL: 'http://localhost:8080/api',
withCredentials: true,
headers: {
'Content-Type': 'application/json',
},
});
// Store access token in memory
let accessToken = null;
export const setAccessToken = (token) => {
accessToken = token;
};
export const getAccessToken = () => accessToken;
// Request interceptor
api.interceptors.request.use(
(config) => {
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor
api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const response = await axios.post(
'http://localhost:8080/api/auth/refresh',
{},
{ withCredentials: true }
);
const { accessToken: newToken } = response.data;
setAccessToken(newToken);
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return api(originalRequest);
} catch (refreshError) {
setAccessToken(null);
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
export default api;
3. Authentication Contextβ
// src/context/AuthContext.jsx
import { createContext, useContext, useState, useEffect } from 'react';
import api, { setAccessToken, getAccessToken } from '../api/axios';
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
checkAuth();
}, []);
const checkAuth = async () => {
try {
const response = await api.post('/auth/refresh');
setAccessToken(response.data.accessToken);
setUser(response.data.user);
} catch (error) {
console.error('Not authenticated');
} finally {
setLoading(false);
}
};
const login = async (email, password, mfaCode = null) => {
const response = await api.post('/auth/login', {
email,
password,
mfaCode,
});
if (response.data.mfaRequired) {
return { mfaRequired: true };
}
setAccessToken(response.data.accessToken);
setUser(response.data.user);
return { success: true };
};
const register = async (data) => {
const response = await api.post('/auth/register', data);
return response.data;
};
const logout = async () => {
try {
await api.post('/auth/logout');
} catch (error) {
console.error('Logout error:', error);
} finally {
setAccessToken(null);
setUser(null);
window.location.href = '/login';
}
};
const verifyEmail = async (email, otp) => {
const response = await api.post('/auth/verify-email', null, {
params: { email, otp },
});
return response.data;
};
const forgotPassword = async (email) => {
const response = await api.post('/auth/forgot-password', null, {
params: { email },
});
return response.data;
};
const resetPassword = async (email, otp, newPassword) => {
const response = await api.post('/auth/reset-password', null, {
params: { email, otp, newPassword },
});
return response.data;
};
const value = {
user,
loading,
login,
register,
logout,
verifyEmail,
forgotPassword,
resetPassword,
hasRole: (role) => user?.roles?.includes(role),
hasPermission: (permission) => user?.permissions?.includes(permission),
};
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
);
}
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
};
4. Protected Route Componentβ
// src/components/ProtectedRoute.jsx
import { Navigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
export function ProtectedRoute({ children, roles = [], permissions = [] }) {
const { user, hasRole, hasPermission } = useAuth();
if (!user) {
return <Navigate to="/login" replace />;
}
if (roles.length > 0 && !roles.some((role) => hasRole(role))) {
return <Navigate to="/unauthorized" replace />;
}
if (
permissions.length > 0 &&
!permissions.some((perm) => hasPermission(perm))
) {
return <Navigate to="/unauthorized" replace />;
}
return children;
}
5. Login Componentβ
// src/pages/Login.jsx
import { useState } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
export function Login() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [mfaCode, setMfaCode] = useState('');
const [showMfa, setShowMfa] = useState(false);
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const { login } = useAuth();
const navigate = useNavigate();
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
setLoading(true);
try {
const result = await login(email, password, showMfa ? mfaCode : null);
if (result.mfaRequired) {
setShowMfa(true);
setLoading(false);
return;
}
if (result.success) {
navigate('/dashboard');
}
} catch (err) {
setError(err.response?.data?.message || 'Login failed');
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full space-y-8 p-8 bg-white rounded-lg shadow">
<div>
<h2 className="text-3xl font-bold text-center">Sign in</h2>
</div>
{error && (
<div className="bg-red-50 border border-red-200 text-red-600 px-4 py-3 rounded">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6">
{!showMfa ? (
<>
<div>
<label className="block text-sm font-medium text-gray-700">
Email
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Password
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
/>
</div>
</>
) : (
<div>
<label className="block text-sm font-medium text-gray-700">
MFA Code
</label>
<input
type="text"
value={mfaCode}
onChange={(e) => setMfaCode(e.target.value)}
required
maxLength={6}
placeholder="Enter 6-digit code"
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
/>
</div>
)}
<button
type="submit"
disabled={loading}
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:opacity-50"
>
{loading ? 'Signing in...' : showMfa ? 'Verify MFA' : 'Sign in'}
</button>
</form>
<div className="text-center space-y-2">
<Link to="/forgot-password" className="text-sm text-blue-600 hover:underline">
Forgot password?
</Link>
<div>
<span className="text-sm text-gray-600">Don't have an account? </span>
<Link to="/register" className="text-sm text-blue-600 hover:underline">
Sign up
</Link>
</div>
</div>
</div>
</div>
);
}
6. Register Componentβ
// src/pages/Register.jsx
import { useState } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
export function Register() {
const [formData, setFormData] = useState({
name: '',
email: '',
password: '',
confirmPassword: '',
phoneNumber: '',
});
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);
const { register } = useAuth();
const navigate = useNavigate();
const handleChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
setLoading(true);
if (formData.password !== formData.confirmPassword) {
setError('Passwords do not match');
setLoading(false);
return;
}
if (formData.password.length < 8) {
setError('Password must be at least 8 characters');
setLoading(false);
return;
}
try {
await register({
name: formData.name,
email: formData.email,
password: formData.password,
phoneNumber: formData.phoneNumber,
});
setSuccess(true);
setTimeout(() => navigate('/verify-email', { state: { email: formData.email } }), 2000);
} catch (err) {
setError(err.response?.data?.message || 'Registration failed');
} finally {
setLoading(false);
}
};
if (success) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full p-8 bg-white rounded-lg shadow text-center">
<div className="text-green-600 text-5xl mb-4">β</div>
<h2 className="text-2xl font-bold mb-2">Registration Successful!</h2>
<p className="text-gray-600">Check your email for verification code.</p>
</div>
</div>
);
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full space-y-8 p-8 bg-white rounded-lg shadow">
<div>
<h2 className="text-3xl font-bold text-center">Create Account</h2>
</div>
{error && (
<div className="bg-red-50 border border-red-200 text-red-600 px-4 py-3 rounded">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700">
Name
</label>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
required
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Email
</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
required
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Phone Number (Optional)
</label>
<input
type="tel"
name="phoneNumber"
value={formData.phoneNumber}
onChange={handleChange}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Password
</label>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
required
minLength={8}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Confirm Password
</label>
<input
type="password"
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleChange}
required
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:opacity-50"
>
{loading ? 'Creating account...' : 'Sign up'}
</button>
</form>
<div className="text-center">
<span className="text-sm text-gray-600">Already have an account? </span>
<Link to="/login" className="text-sm text-blue-600 hover:underline">
Sign in
</Link>
</div>
</div>
</div>
);
}
7. Email Verification Componentβ
// src/pages/VerifyEmail.jsx
import { useState } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
export function VerifyEmail() {
const [otp, setOtp] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const { verifyEmail } = useAuth();
const navigate = useNavigate();
const location = useLocation();
const email = location.state?.email;
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
setLoading(true);
try {
await verifyEmail(email, otp);
navigate('/login');
} catch (err) {
setError(err.response?.data?.message || 'Verification failed');
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full space-y-8 p-8 bg-white rounded-lg shadow">
<div>
<h2 className="text-3xl font-bold text-center">Verify Email</h2>
<p className="mt-2 text-center text-sm text-gray-600">
Enter the 6-digit code sent to {email}
</p>
</div>
{error && (
<div className="bg-red-50 border border-red-200 text-red-600 px-4 py-3 rounded">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700">
Verification Code
</label>
<input
type="text"
value={otp}
onChange={(e) => setOtp(e.target.value)}
required
maxLength={6}
placeholder="000000"
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md text-center text-2xl tracking-widest"
/>
</div>
<button
type="submit"
disabled={loading || otp.length !== 6}
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:opacity-50"
>
{loading ? 'Verifying...' : 'Verify Email'}
</button>
</form>
</div>
</div>
);
}
8. Dashboard Componentβ
// src/pages/Dashboard.jsx
import { useAuth } from '../context/AuthContext';
export function Dashboard() {
const { user, logout, hasRole, hasPermission } = useAuth();
return (
<div className="min-h-screen bg-gray-50">
<nav className="bg-white shadow">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16">
<div className="flex items-center">
<h1 className="text-xl font-bold">Dashboard</h1>
</div>
<div className="flex items-center space-x-4">
<span className="text-gray-700">{user?.name}</span>
<button
onClick={logout}
className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
>
Logout
</button>
</div>
</div>
</div>
</nav>
<main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<div className="px-4 py-6 sm:px-0">
<div className="bg-white shadow rounded-lg p-6">
<h2 className="text-2xl font-bold mb-4">Welcome, {user?.name}!</h2>
<div className="space-y-4">
<div>
<h3 className="text-lg font-semibold">User Information</h3>
<dl className="mt-2 space-y-2">
<div>
<dt className="text-sm font-medium text-gray-500">Email</dt>
<dd className="text-sm text-gray-900">{user?.email}</dd>
</div>
<div>
<dt className="text-sm font-medium text-gray-500">Email Verified</dt>
<dd className="text-sm text-gray-900">
{user?.emailVerified ? (
<span className="text-green-600">β Verified</span>
) : (
<span className="text-red-600">β Not Verified</span>
)}
</dd>
</div>
<div>
<dt className="text-sm font-medium text-gray-500">MFA Enabled</dt>
<dd className="text-sm text-gray-900">
{user?.mfaEnabled ? (
<span className="text-green-600">β Enabled</span>
) : (
<span className="text-gray-600">β Disabled</span>
)}
</dd>
</div>
</dl>
</div>
<div>
<h3 className="text-lg font-semibold">Roles</h3>
<div className="mt-2 flex flex-wrap gap-2">
{user?.roles?.map((role) => (
<span
key={role}
className="px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm"
>
{role}
</span>
))}
</div>
</div>
<div>
<h3 className="text-lg font-semibold">Permissions</h3>
<div className="mt-2 flex flex-wrap gap-2">
{user?.permissions?.map((permission) => (
<span
key={permission}
className="px-3 py-1 bg-green-100 text-green-800 rounded-full text-sm"
>
{permission}
</span>
))}
</div>
</div>
{hasRole('ADMIN') && (
<div className="mt-6 p-4 bg-yellow-50 border border-yellow-200 rounded">
<h3 className="text-lg font-semibold text-yellow-800">
Admin Section
</h3>
<p className="text-sm text-yellow-700 mt-2">
You have admin privileges and can access admin features.
</p>
</div>
)}
{hasPermission('user:create') && (
<div className="mt-6 p-4 bg-purple-50 border border-purple-200 rounded">
<h3 className="text-lg font-semibold text-purple-800">
User Management
</h3>
<p className="text-sm text-purple-700 mt-2">
You can create and manage users.
</p>
</div>
)}
</div>
</div>
</div>
</main>
</div>
);
}
9. App Router Setupβ
// src/App.jsx
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { AuthProvider } from './context/AuthContext';
import { ProtectedRoute } from './components/ProtectedRoute';
import { Login } from './pages/Login';
import { Register } from './pages/Register';
import { VerifyEmail } from './pages/VerifyEmail';
import { Dashboard } from './pages/Dashboard';
function App() {
return (
<BrowserRouter>
<AuthProvider>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/verify-email" element={<VerifyEmail />} />
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
<Route
path="/admin"
element={
<ProtectedRoute roles={['ADMIN']}>
<div>Admin Page</div>
</ProtectedRoute>
}
/>
<Route path="/" element={<Navigate to="/dashboard" replace />} />
<Route path="/unauthorized" element={<div>Unauthorized</div>} />
</Routes>
</AuthProvider>
</BrowserRouter>
);
}
export default App;
Security Best Practicesβ
Password Securityβ
// Password validation
@Pattern(
regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$",
message = "Password must contain at least 8 characters, one uppercase, one lowercase, one number and one special character"
)
private String password;
// Password strength checker (JavaScript)
function checkPasswordStrength(password) {
let strength = 0;
if (password.length >= 8) strength++;
if (password.length >= 12) strength++;
if (/[a-z]/.test(password) && /[A-Z]/.test(password)) strength++;
if (/\d/.test(password)) strength++;
if (/[@$!%*?&]/.test(password)) strength++;
const levels = ['Weak', 'Fair', 'Good', 'Strong', 'Very Strong'];
return levels[strength];
}
Token Securityβ
Best Practices:
- β Use HTTPS in production
- β Short-lived access tokens (15 min)
- β Long-lived refresh tokens (7-30 days)
- β Store refresh tokens in HttpOnly cookies
- β Implement token rotation
- β Use secure random secrets (256-bit minimum)
- β Implement token blacklisting for logout
API Securityβ
// Rate limiting with Bucket4j
@Component
public class RateLimitInterceptor implements HandlerInterceptor {
private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String key = request.getRemoteAddr();
Bucket bucket = buckets.computeIfAbsent(key, k -> createBucket());
if (bucket.tryConsume(1)) {
return true;
}
response.setStatus(429);
response.getWriter().write("{\"error\": \"Too many requests\"}");
return false;
}
private Bucket createBucket() {
return Bucket.builder()
.addLimit(Bandwidth.simple(100, Duration.ofMinutes(1)))
.build();
}
}
Common Vulnerabilitiesβ
| Vulnerability | Protection |
|---|---|
| XSS | HttpOnly cookies, CSP headers, input sanitization |
| CSRF | SameSite cookies, CSRF tokens |
| SQL Injection | Parameterized queries, ORM |
| Brute Force | Rate limiting, account lockout, CAPTCHA |
| Session Fixation | Regenerate session on login |
| Man-in-the-Middle | HTTPS only, secure cookies |
Production Checklistβ
Environment Variablesβ
# Production application.properties
server.port=8080
server.ssl.enabled=true
server.ssl.key-store=classpath:keystore.p12
server.ssl.key-store-password=${SSL_PASSWORD}
server.ssl.key-store-type=PKCS12
spring.datasource.url=${DB_URL}
spring.datasource.username=${DB_USERNAME}
spring.datasource.password=${DB_PASSWORD}
jwt.secret=${JWT_SECRET}
jwt.access-token-expiration=900000
jwt.refresh-token-expiration=604800000
cookie.secure=true
cookie.http-only=true
cookie.same-site=Strict
spring.mail.host=${MAIL_HOST}
spring.mail.port=${MAIL_PORT}
spring.mail.username=${MAIL_USERNAME}
spring.mail.password=${MAIL_PASSWORD}
Deployment Checklistβ
- β Enable HTTPS/SSL
- β Set secure cookie flags
- β Use environment variables for secrets
- β Enable CORS for specific origins only
- β Implement rate limiting
- β Set up monitoring and logging
- β Enable database encryption
- β Use password strength requirements
- β Implement account lockout policies
- β Set up automated backups
- β Enable audit logging
- β Configure firewall rules
- β Use CDN for static assets
- β Implement health checks
- β Set up error tracking (Sentry, etc.)
Conclusionβ
This guide provides a complete, production-ready authentication and authorization system using Spring Boot and React. The implementation includes JWT tokens, RBAC, MFA, OTP verification, and follows security best practices.
Key Features:
- β Secure JWT authentication with refresh tokens
- β Role-Based Access Control (RBAC)
- β Multi-Factor Authentication (MFA)
- β Email verification with OTP
- β Password reset functionality
- β HttpOnly cookies for token storage
- β Protected routes and API endpoints
- β Security best practices implemented
Next Steps:
- Implement OAuth2 SSO (Google, GitHub)
- Add biometric authentication
- Implement passwordless authentication
- Add session management dashboard
- Set up audit logging
- Implement device fingerprinting
- Add backup codes for MFA recovery
For production deployment, ensure all security measures are in place and thoroughly test all authentication flows.